package com.inexas.cu.stratus;

import java.lang.reflect.*;
import java.util.*;
import javax.jdo.annotations.*;
import com.google.inject.*;

/**
 * This is an in memory 'database' storage that mimics Google's Bigtable storage
 * used in the Google App Engine. It 'hides' behind the Bigtable interface so we
 * don't have to implement the full feature set.
 */
@Singleton
public class MockStratus implements Stratus {
	private final Map<String, Map<Long, Entity>> types = //
	new HashMap<String, Map<Long, Entity>>();
	private final Set<Class<? extends Stratus>> checked = new HashSet<Class<? extends Stratus>>();
	private long nextId = 1;

	public void create(Entity entity) {
		checkEntity(entity);

		if(entity.getIdent() != null) {
			throw new StratusException("Entity already has ID: " + entity + ", " + entity.getIdent());
		}

		final Map<Long, Entity> typeMap = getTypeMap(entity.getClass());
		final Long ident = new Long(nextId++);
		typeMap.put(ident, entity);

		// Set the ident...
		try {
			final Field identField = entity.getClass().getDeclaredField("ident");
			identField.setAccessible(true); // Subverts 'private' modifier
			identField.set(entity, ident);
		} catch(final Exception e) {
			throw new StratusException(entity, "Error setting ident", e);
		}
//		System.out.println("St: created" + entity);
	}

	@SuppressWarnings("unchecked")
	public <T> T findByIdent(Class<T> type, Long ident) {
		final T result;
		checkType(type);

		if(ident == null) {
			throw new StratusException("Null identity (Long ident)");
		}

		final Map<Long, Entity> typeMap = getTypeMap(type);
		if(typeMap == null) {
			result = null;
		} else {
			result = (T)typeMap.get(ident);
		}

		return result;
	}

	@SuppressWarnings("unchecked")
	public <T> Iterator<T> findAll(Class<T> type) {
		checkType(type);

		final Map<Long, Entity> typeMap = getTypeMap(type);
		return (Iterator<T>)typeMap.values().iterator();
	}

	public <T> T findBy(Class<T> type, String... constraints) {
		final List<? extends T> list = findAllBy(type, constraints);
		return list.size() == 0 ? null : list.get(0);
	}

	public <T> List<T> findAllBy(Class<T> type, String... constraints) {
		final Iterator<T> all = findAll(type);
		final Constraints<T> constraint = new Constraints<T>(constraints);
		final List<T> result = constraint.constrain(all);
		return result;
	}

	public void update(Entity entity) throws NoSuchEntityException {
		checkEntity(entity);

		final Long ident = entity.getIdent();
		if(ident == null) {
			throw new StratusException(entity, "Entity has not been persisted: null ident");
		}

		final Map<Long, Entity> typeMap = getTypeMap(entity.getClass());
		if(typeMap == null) {
			throw new StratusException(entity, "Entity has not been persisted");
		}

		if(typeMap.get(ident) == null) {
			throw new StratusException(entity, "Entity not found in table, maybe you deleted it?");
		}

		typeMap.put(ident, entity);
//		System.out.println("St: updated" + entity);
	}

	public void delete(Entity entity) throws NoSuchEntityException {
		checkEntity(entity);

		final Long ident = entity.getIdent();
		if(ident == null) {
			throw new StratusException(entity, "Entity has not been persisted: null ident");
		}

		final Map<Long, Entity> typeMap = getTypeMap(entity.getClass());
		if(typeMap == null) {
			throw new StratusException(entity, "Entity has not been persisted");
		}

		if(typeMap.get(ident) == null) {
			throw new StratusException(entity, "Entity not found in table, maybe you deleted it already?");
		}

		typeMap.remove(ident);
//		System.out.println("St: deleted" + entity);
	}

	public void deleteAll(Class<? extends Entity> type) {
		checkType(type);

		final Map<Long, Entity> typeMap = getTypeMap(type);
		typeMap.clear();
	}

	private void checkEntity(Entity entity) {
		if(entity == null) {
			throw new StratusException("Null entity");
		}
		checkType(entity.getClass());
	}

	private void checkType(Class<?> type) {
		if(type == null) {
			throw new StratusException("Null type");
		}

		if(type.isAssignableFrom(Entity.class)) {
			throw new StratusException( //
			        type, //
			        "Class does not implement BigtableEntity");
		}

		if(!checked.contains(type)) {
			// @PersistenceCapable(identityType = IdentityType.APPLICATION,
			// detachable="true")
			final PersistenceCapable pc = type.getAnnotation(PersistenceCapable.class);

			if(pc == null) {
				throw new StratusException( //
				        type, //
				        "Class must be annotated with @PersistenceCapable");
			}

			if(!pc.identityType().equals(IdentityType.APPLICATION)) {
				throw new StratusException( //
				        type, //
				        "Class not defined as 'identityType = IdentityType.APPLICATION' " + //
				                "(see @PersistenceCapable)");
			}

			if(!pc.detachable().equals("true")) {
				throw new StratusException( //
				        type, //
				        "Class not defined as detachable (see @PersistenceCapable)");
			}

			// Check for the existence of the Long ident field
			try {
				final Field identField = type.getDeclaredField("ident");
				if(identField.getType() != Long.class) {
					throw new StratusException( //
					        type, //
					        "The field ident must be defined as a Long, e.g. 'private Long ident;");
				}
				final int modifiers = identField.getModifiers();
				if((modifiers & (Modifier.STATIC | Modifier.FINAL)) != 0) {
					throw new StratusException(type, "The field ident must not be static or final");
				}
			} catch(final Exception e) {
				throw new StratusException( //
				        type, //
				        "The field ident is not defined, e.g. 'private Long ident;", //
				        e);
			}
		}
	}

	private Map<Long, Entity> getTypeMap(Class<?> type) {
		Map<Long, Entity> result;

		final String typeKey = type.getName();
		result = types.get(typeKey);
		if(result == null) {
			result = new HashMap<Long, Entity>();
			types.put(typeKey, result);
		}
		return result;
	}

}
